package database

import (
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/pkg/errors"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"sensi/common/frame"
	"sensi/common/sys"
	"time"
)

// DB ...
var DB *gorm.DB

const (
	database    = "database"
	transaction = "tx"
)

const (
	driverMysql = "mysql"
)

// Config ...
type Config struct {
	Driver          string `yaml:"driver"`
	User            string `yaml:"user"`
	Password        string `yaml:"password"`
	Host            string `yaml:"host"`
	Port            string `yaml:"port"`
	DatabaseName    string `yaml:"database_name"`
	Charset         string `yaml:"charset"`
	MaxIdleConns    int    `yaml:"max_idle_conns"`
	MaxOpenConns    int    `yaml:"max_open_conns"`
	ConnMaxLifetime int    `yaml:"conn_max_lifetime"`
}

// DefaultConfig ...
func DefaultConfig() *Config {
	return &Config{
		Driver:          "mysql",
		Port:            "3306",
		Charset:         "utf8",
		MaxIdleConns:    10,
		MaxOpenConns:    100,
		ConnMaxLifetime: 3600,
	}
}

// Middleware ...
func Middleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Set("database", DB)
	}
}

// Setup ...
func Setup(setting *Config) error {
	switch setting.Driver {
	case driverMysql, "":
		if setting.User == "" ||
			setting.Password == "" ||
			setting.Host == "" ||
			setting.Port == "" ||
			setting.DatabaseName == "" ||
			setting.Charset == "" {
			return errors.New("invalid database setting")
		}

		dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s",
			setting.User,
			setting.Password,
			setting.Host,
			setting.Port,
			setting.DatabaseName,
			setting.Charset,
		)

		loggerConfig := logger.Config{
			SlowThreshold: 200 * time.Millisecond,
			LogLevel:      logger.Error,
			Colorful:      false,
		}
		if sys.Env == frame.EnvDevelopment {
			loggerConfig.LogLevel = logger.Info
			loggerConfig.Colorful = true
		}

		db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
			Logger: logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), loggerConfig),
		})
		if err != nil {
			return err
		}

		sqlDB, err := db.DB()
		if err != nil {
			return err
		}
		sqlDB.SetMaxIdleConns(setting.MaxIdleConns)
		sqlDB.SetMaxOpenConns(setting.MaxOpenConns)
		sqlDB.SetConnMaxLifetime(time.Duration(setting.ConnMaxLifetime) * time.Second)

		DB = db
	default:
		return errors.New(fmt.Sprintf("no support database driver: %s", setting.Driver))
	}

	return nil
}

// Begin ...
var Begin = func(ctx context.Context) context.Context {
	db := fromContext(ctx, database)
	if db == nil {
		panic("nil database in context")
	}
	tx := db.Begin()
	return context.WithValue(ctx, transaction, tx)
}

// Commit ...
var Commit = func(ctx context.Context) context.Context {
	tx := fromContext(ctx, transaction)
	if tx == nil {
		panic("nil transaction in context")
	}
	tx.Commit()
	return context.WithValue(ctx, transaction, nil)
}

// Rollback ...
var Rollback = func(ctx context.Context) context.Context {
	tx := fromContext(ctx, transaction)
	if tx == nil {
		panic("nil transaction in context")
	}
	tx.Rollback()
	return context.WithValue(ctx, transaction, nil)
}

// Instance ...
func Instance(ctx context.Context) *gorm.DB {
	if db := fromContext(ctx, transaction); db != nil {
		return db
	}
	return fromContext(ctx, database)
}

func fromContext(ctx context.Context, name string) *gorm.DB {
	v := ctx.Value(name)
	if db, ok := v.(*gorm.DB); ok {
		return db
	}
	return nil
}
